﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using SharpObjects.SearchEngine.HtmlParsing.Html;

namespace SharpObjects.SearchEngine.HtmlParsing.Parser
{
    public class HtmlParser
    {
        private string html;
        private int currentPosition = 0;
        private HtmlTagFactory tagFactory = new HtmlTagFactory();

        public HtmlParser(string html = null)
        {
            if(html == null)
            {
                html = string.Empty;
            }

            this.html = html;
        }

        public HtmlParserDocument Parse()
        {
            //this.html = "<head>" +
            //    "<style type=\"text/css\"> " +
            //    "* html #hdr-auth," +
            //    "* html #pmUserPanel" +
            //    "{display:none} " +
            //    ".cnn_shdcamtt1 .cnn_mtlplnode { height:124px; }" +
            //    "</style> " +
            //    "<meta name='keywords' content=\"test, parser test, some text\"/>" +
            //    "<meta name='description' content=\"A concise tutorial of HTML tags and how to code them.\" />" +
            //    "</head>" +
            //    "<body> some<br/> text" +
            //    "<!--include virtual=\"/cnn_adspaces/3.0/homepage/main/top.1x1.ad\" -->" +
            //    "<a href=\"http://news.blogs.cnn.com/?hpt=T2\">This Just In: CNN's news blog</a>" +
            //    " <p>some <b>paragraph</b> text</p></body>";

            //this.html = "<body>" +
            //            "<div><a href=\"/\">Home</a> | <a href=\"/video/\">Video</a> | <a href=\"http://newspulse.cnn.com/\">NewsPulse</a> | <a href=\"/US/\">U.S.</a> | <a href=\"/WORLD/\">World</a> | <a href=\"/POLITICS/\">Politics</a> | <a href=\"/JUSTICE/\">Justice</a> | <a href=\"/SHOWBIZ/\">Entertainment</a> | <a href=\"/TECH/\">Tech</a> | <a href=\"/HEALTH/\">Health</a> | <a href=\"/LIVING/\">Living</a> | <a href=\"/TRAVEL/\">Travel</a> | <a href=\"/OPINION/\">Opinion</a> | <a href=\"http://ireport.cnn.com/?cnn=yes\">iReport</a> | <a href=\"http://money.cnn.com/?cnn=yes\">Money</a> | <a href=\"http://sportsillustrated.cnn.com/?xid=cnnfoot\">Sports</a></div>" +
            //            "<div><a href=\"/tools/index.html\" title=\"\">Tools &amp; widgets</a> | <a href=\"/services/rss/\" title=\"\">RSS</a> | <a href=\"/services/podcasting/\" title=\"\">Podcasts</a> | <a href=\"/exchange/blogs/index.html\" title=\"\">Blogs</a> | <a href=\"/mobile/\" title=\"\">CNN mobile</a> | <a href=\"/profile/\" title=\"\">My profile</a> | <a href=\"/profile/\" title=\"\">E-mail alerts</a> <!--| <a href=\"/audio/radio/winmedia.html\" title=\"\">CNN Radio</a>--> | <a href=\"http://www.turnerstoreonline.com/\" title=\"\">CNN shop</a> | <a href=\"/sitemap/\" title=\"\">Site map</a></div>" +
            //            "</div >" +
            //            "</body>";

            //this.html = "<body>" +
            //            "<table><caption>Test Table</caption>" +
            //            "<tr><th>Column 1</th><th>Column 2</th></tr>" +
            //            "<tr><td><p>cell 1.1</p></td><td>cell 1.2</td></tr>" +
            //            "<tr><td>cell 2.1</td><td>cell 2.2</td></tr>" +
            //            "</table>" +
            //            "</body>";

            //this.html = "<body>my text</body>";

            //this.html = "<body>some text <b>with bold</b></body>";

            HtmlParserDocument htmlParserDocument = new HtmlParserDocument(this.tagFactory);
            HtmlGenericTag htmlGenericTag;

            while((htmlGenericTag = GetNextTag()) != null)
            {
                htmlParserDocument.Add(htmlGenericTag);
            }

            return htmlParserDocument;
        }

        private char CurrentCharacter
        {
            get
            {
                return this.html[this.currentPosition];
            }
        }

        private const string commentOpenTag = "<!--";
        private const string commentCloseTag = "-->";

        private HtmlGenericTag ProcessComment()
        {
            if ((this.html.Length - this.currentPosition) > commentOpenTag.Length
                && this.html[this.currentPosition] == '<'
                && this.html[this.currentPosition + 1] == '!'
                && this.html[this.currentPosition + 2] == '-'
                && this.html[this.currentPosition + 3] == '-')
            {
                // found a comment
                this.currentPosition += commentOpenTag.Length;
                int commentStart = this.currentPosition;
                int commentEnd = -1;

                for (; this.currentPosition < this.html.Length; this.currentPosition++)
                {
                    if ((this.html.Length - this.currentPosition) > commentCloseTag.Length
                        && this.html[this.currentPosition] == '-'
                        && this.html[this.currentPosition + 1] == '-'
                        && this.html[this.currentPosition + 2] == '>')
                    {
                        // found end of comment
                        commentEnd = this.currentPosition - 1;
                        this.currentPosition += commentCloseTag.Length;
                        break;
                    }
                }

                if (commentEnd == -1)
                {
                    commentEnd = this.html.Length - commentStart;
                }

                HtmlGenericTag commentTag = new HtmlGenericTag() { IsClosed = true, Name = commentOpenTag.Substring(1) };

                if (commentEnd > commentStart)
                {
                    commentTag.Text = this.html.Substring(commentStart, commentEnd - commentStart);
                }

                return commentTag;
            }
            else
            {
                return null;
            }
        }

        private HtmlGenericTag GetNextTag()
        {
            HtmlGenericTag tag = new HtmlGenericTag();
            FindResult findResult = FindNextNonWhiteSpace(true);

            if(findResult.IsEof || !findResult.IsFound)
            {
                return null;
            }

            if (this.CurrentCharacter == '<')
            {
                // found a tag

                HtmlGenericTag commentTag = ProcessComment();

                if(commentTag != null)
                {
                    return commentTag;
                }

                IncrementCurrentPosition(); // moving to the tag name beginning
                int start = this.currentPosition;
                findResult = FindNextCharacter(new TagNameEndCondition(), true); // white space or '>'

                if (findResult.IsEof || !findResult.IsFound)
                {
                    return null;
                }

                tag.Name = this.html.Substring(start, this.currentPosition - start).Trim().ToLower();

                if(tag.Name.StartsWith("/"))
                {
                    tag.Name = tag.Name.Substring(1);
                    tag.IsClosing = true; // found closing portion of the tag
                }

                if(tag.Name.EndsWith("/"))
                {
                    tag.Name = tag.Name.Substring(0, tag.Name.Length - 1);
                    tag.IsClosed = true; // tag has no body and is closed
                }

                start = this.currentPosition;
                findResult = FindNextTagBracket(eTagBracketType.Ending);
                string attributeString = this.html.Substring(start, this.currentPosition - start).Trim();

                if(attributeString.EndsWith("/"))
                {
                    attributeString = attributeString.Substring(0, attributeString.Length - 1);
                }

                ParseAndAddAttributes(tag, attributeString);

                if(this.currentPosition > 0 && this.html[this.currentPosition - 1] == '/')
                {
                    tag.IsClosed = true; // tag is closed after the attributes
                }

                IncrementCurrentPosition();
            }
            else
            {
                // found text
                int start = this.currentPosition;
                findResult = FindNextTagBracket(eTagBracketType.Opening);

                tag.Text = this.html.Substring(start, this.currentPosition - start).Trim();
                tag.Text = System.Web.HttpUtility.HtmlDecode(tag.Text);
                tag.Name = "text";
            }

            return tag;
        }

        private void ParseAndAddAttributes(HtmlTag tag, string attributes)
        {
            if (!string.IsNullOrEmpty(attributes))
            {
                HtmlParser attributeParser = new HtmlParser(attributes);
                FindResult findResult = attributeParser.FindNextNonWhiteSpace(true); 

                while (!findResult.IsEof)
                {
                    findResult = attributeParser.FindNextNonWhiteSpace(true);

                    if (findResult.IsEof || !findResult.IsFound)
                    {
                        break;
                    }
                    
                    int start = attributeParser.currentPosition;
                    findResult = attributeParser.FindNextCharacter(new MatchCharacterCondition('='), true);

                    if(findResult.IsEof || !findResult.IsFound)
                    {
                        break;
                    }

                    HtmlAttribute attribute = new HtmlAttribute();

                    attribute.Name = attributes.Substring(start, attributeParser.currentPosition - start).Trim().ToLower();

                    attributeParser.IncrementCurrentPosition();
                    findResult = attributeParser.FindNextNonWhiteSpace(true);

                    if (findResult.IsEof || !findResult.IsFound)
                    {
                        break;
                    }

                    start = attributeParser.currentPosition;
                    findResult = attributeParser.FindNextWhiteSpace(false);
                    attribute.Value = attributes.Substring(start, attributeParser.currentPosition - start).Trim();
                    attribute.Value = TrimQuotes(attribute.Value);

                    tag.AddAttribute(attribute);
                }
            }
        }

        private string TrimQuotes(string value)
        {
            if (value.StartsWith("'") || value.StartsWith("\""))
            {
                if (value.Length > 0)
                {
                    value = value.Substring(1);
                }
            }

            if (value.EndsWith("'") || value.EndsWith("\""))
            {
                if (value.Length > 0)
                {
                    value = value.Substring(0, value.Length - 1);
                }
            }

            return value;
        }


        private enum eTagBracketType
        {
            Opening,
            Ending
        }

        private void IncrementCurrentPosition()
        {
            if (this.currentPosition < this.html.Length)
            {
                this.currentPosition++;
            }
        }

        private interface IParsingCondition
        {
            bool Evaluate(char ch);
        }

        private class MatchCharacterCondition : IParsingCondition
        {
            private char ch;

            public MatchCharacterCondition(char ch)
            {
                this.ch = ch;
            }

            #region IParsingCondition Members

            public bool Evaluate(char ch)
            {
                return ch == this.ch;
            }

            #endregion
        }

        private class WhitespaceCondition : IParsingCondition
        {
            private bool isReversed;

            public WhitespaceCondition(bool isReversed = false)
            {
                this.isReversed = isReversed;
            }

            #region IParsingCondition Members

            public bool Evaluate(char ch)
            {
                if(isReversed)
                {
                    return !char.IsWhiteSpace(ch);
                }
                else
                {
                    return char.IsWhiteSpace(ch);
                }
            }

            #endregion
        }

        private class TagNameEndCondition : IParsingCondition
        {

            #region IParsingCondition Members

            public bool Evaluate(char ch)
            {
                if(char.IsWhiteSpace(ch) || ch == '>')
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }

            #endregion
        }


        private FindResult FindNextCharacter(IParsingCondition condition, bool ignoreQuotes)
        {
            FindResult findResult = new FindResult() { IsEof = false, IsFound = false };

            Stack<char> inString = new Stack<char>();

            for (; this.currentPosition < this.html.Length; this.currentPosition++)
            {
                if (inString.Count == 0)
                {
                    if(condition.Evaluate(this.html[this.currentPosition]))
                    {
                        findResult.IsFound = true;
                        return findResult;
                    }

                    if (!ignoreQuotes)
                    {
                        if (this.html[this.currentPosition] == '\'' || this.html[this.currentPosition] == '"')
                        {
                            inString.Push(this.html[this.currentPosition]);
                        }
                    }
                }
                else
                {
                    if (!ignoreQuotes)
                    {
                        if (this.html[this.currentPosition] == inString.Peek())
                        {
                            inString.Pop();
                        }
                    }
                }
            }

            findResult.IsEof = true;
            return findResult;
        }

        private FindTagBracketResult FindNextTagBracket(eTagBracketType bracketType)
        {
            char bracket;
            bool ignoreQuotes;

            if(bracketType == eTagBracketType.Opening)
            {
                bracket = '<';
                ignoreQuotes = true;
            }
            else
            {
                bracket = '>';
                ignoreQuotes = false;
            }

            FindResult findResult = FindNextCharacter(new MatchCharacterCondition(bracket), ignoreQuotes);
            FindTagBracketResult findTagBracketResult = new FindTagBracketResult(findResult){ IsEndOfTag = false};

            if(bracketType == eTagBracketType.Opening)
            {
                if(this.currentPosition > 0)
                {
                    if(this.html[this.currentPosition - 1] == '/')
                    {
                        findTagBracketResult.IsEndOfTag = true;
                    }
                }
            }

            return findTagBracketResult;
        }

        private FindResult FindNextWhiteSpace(bool ignoreQuotes)
        {
            return FindNextCharacter(new WhitespaceCondition(), ignoreQuotes);
        }

        private FindResult FindNextNonWhiteSpace(bool ignoreQuotes)
        {
            return FindNextCharacter(new WhitespaceCondition(true), ignoreQuotes);
        }

        private FindResult FindEndOfTagName()
        {
            return FindNextCharacter(new TagNameEndCondition(), true);
        }

        private string GetTagName()
        {
            int beginning = this.currentPosition;
            FindResult findResult = FindEndOfTagName();
            return this.html.Substring(beginning, this.currentPosition - beginning).Trim().ToLower();
        }
    }
}
